import { Command } from "commander";
import { IHeapNode } from "@memlab/core";
import { 
    ILoggable, Log, 
    IAnalyzer, ArkStatAnalyzer, ArkStatCfg, defaultArkStatCfg, ArkSerializer, ArkNodeTracePath,
    IOutput, OutputCfg, OutputFactory, OutputType,
    JsonReader,
    ArkNodesMap
} from "@meminsight/core";
import { IProcess } from "./IProcess";
import { Version } from "../Version";

/**
 * Ark Statistics Process
 */
export class ArkStatProc implements IProcess, ILoggable { 
    public static readonly CMD: string = 'ark-stat';
    public static readonly DESC: string = "ark statistic analysis process.";
    public static readonly VER: string = Version.ARK_STAT.toString();
    private static _instance: ArkStatProc;

    public readonly DOMAIN: string = 'meminsight';
    public readonly TAG: string = ArkStatProc.name;
    private analyzer: IAnalyzer = new ArkStatAnalyzer();

    /**
     * get instance
     * @returns instance
     */
    public static instance(): ArkStatProc {
        if (!ArkStatProc._instance) {
            ArkStatProc._instance = new ArkStatProc();
        }
        return ArkStatProc._instance;
    }

    public initialize(proc: Command, any: any) {
        proc.command(ArkStatProc.CMD)
            .description(ArkStatProc.DESC)
            .requiredOption("-t, --target <target heapsnapshot file>", "target heapsnapshot file")
            .requiredOption(
                "-m, --mode <mode type>", 
                "\nmode type:\n" +
                "\t1: get all detached nodes\n" + 
                "\t2: get all gc roots\n" + 
                "\t3: get node shortest path, node id is required\n" + 
                "\t4: get node aggration paths by configuration\n" + 
                "\t5: get node by configuration\n" + 
                "\t6: get shared heap nodes\n")
            .option("-i, --node_id <node id>", "node id")
            .option("-c, --config <configuration file>", "configuration file")
            .option("-o, --output <output folder path>", "output folder path", "./output")
            .option("-f, --output_file <output file type>",
                    "output file type, console | txt | json | html.",
                    "console")
            .action(async (options) => {
                await ArkStatProc.instance().process(new Map<string, any>([
                    ["target", options.target],
                    ["mode", options.mode],
                    ["node_id", options.node_id],
                    ["config", options.config || undefined],
                    ["output", options.output],
                    ["output_file", options.output_file],
                ]));
            });
    }

    private async process(args: Map<string, string | undefined>): Promise<void> {
        let target = args.get("target");
        if (!target) {
            Log.errorX(this, "Target filepath is required");
            return;
        }
        let result = await this.analyzer.analyze([target]);
        if (!result) {
            Log.errorX(this, "Faled to analyze.");
            return
        }
        Log.infoX(this, "Analyzed successfully.");

        let cfgfile = args.get('config');
        let setResult: boolean = false;
        if (cfgfile) {
            let cfg = await JsonReader.instance().readFromFile<ArkStatCfg>(cfgfile);
            if (cfg) {
                this.analyzer.setConfig(cfg);
                setResult = true;
            }
        }
        if (!setResult) {
            Log.warnX(this, "No configuration found, use default config.");
            this.analyzer.setConfig(defaultArkStatCfg());
        }
        console.log(this.analyzer.getConfig());
        Log.infoX(this, "Set configuration successfully.");

        let mode = args.get("mode") || '1';
        let outputDir: string = args.get("output") || './output';
        let outputFileType: string = args.get("output_file") || 'console';
        let nodeId: number = parseInt(args.get("node_id") || '1');
        Log.infoX(this, `mode:${mode}; output:${outputDir}; type:${outputFileType}`);

        this.takeActionByMode(mode, outputDir, outputFileType, nodeId);
    }

    private getOutputCfg(
        output: IOutput, 
        outputDesc: any, 
        outputDir: string, 
        outputFile: string, 
        enableNode: boolean = true) : OutputCfg { 
        const analyzerCfg = this.analyzer.getConfig() as ArkStatCfg;
        let cfg: OutputCfg = {
            desc: outputDesc,
            output: {
                dir: outputDir,
                filename: outputFile,
                extension: output.type.toString(),
            },  
            node: enableNode ? {
                enableId: analyzerCfg.Node.enableId,
                enableType: analyzerCfg.Node.enableType,
                enableName: analyzerCfg.Node.enableName,
                enableSelfSize: analyzerCfg.Node.enableSelfSize,
                enableRetainedSize: analyzerCfg.Node.enableRetainedSize,
            } : undefined
        }
        return cfg;
    }

    private async outputNodes(
        nodes: IHeapNode[], 
        outputEntity: IOutput, 
        outputCfg: OutputCfg): Promise<void> {
        if (outputEntity?.type  === OutputType.CONSOLE) {
            outputEntity?.output(ArkSerializer.getConsoleDataByNodes(true, nodes, outputCfg), outputCfg);
        } else if (outputEntity?.type  === OutputType.TEXT) {
            outputEntity?.output(ArkSerializer.getTextDataByNodes(nodes, outputCfg), outputCfg);
        } else if (outputEntity?.type  === OutputType.JSON) {
            outputEntity?.output(ArkSerializer.getJsonDataByNodes(nodes, outputCfg), outputCfg);
        } else if (outputEntity?.type  === OutputType.HTML) {
            // TODO
            outputEntity?.output(ArkSerializer.getHtmlDataByNodes(nodes, outputCfg), outputCfg);
        } else {
            Log.errorX(this, 'Invalid output file type.');
        }
    }

    private async outputArkNodeTracePath(
        path: ArkNodeTracePath, 
        outputEntity: IOutput, 
        outputCfg: OutputCfg): Promise<void> {
        if (outputEntity?.type  === OutputType.CONSOLE) {
            let dataTmp: string = ArkSerializer.getNodeTracePathString(path.tracePathItem);
            outputEntity?.output(dataTmp, outputCfg);
        } else if (outputEntity?.type  === OutputType.TEXT) {
            let dataTmp = {
                desc: outputCfg.desc,
                data: ArkSerializer.getNodeTracePathString(path.tracePathItem)
            };
            outputEntity?.output(dataTmp, outputCfg);
        } else if (outputEntity?.type  === OutputType.JSON) {
            let dataTmp = {
                desc: outputCfg.desc,
                data: ArkSerializer.getNodeTracePathString(path.tracePathItem)
            };
            outputEntity?.output(dataTmp, outputCfg);
        } else if (outputEntity?.type  === OutputType.HTML) {
            // TODO
        } else {
            Log.errorX(this, 'Invalid output file type.');
        }
    }

    private async outputAggrationPath(
        paths: ArkNodesMap, 
        outputEntity: IOutput, 
        outputCfg: OutputCfg) {
        if (outputEntity?.type  === OutputType.CONSOLE) {
            let dataTmp: string = ArkSerializer.getArkNodesMapString(paths);
            outputEntity?.output(dataTmp, outputCfg);
        } else if (outputEntity?.type  === OutputType.TEXT) {
            // TODO
        } else if (outputEntity?.type  === OutputType.JSON) {
            // TODO
        } else if (outputEntity?.type  === OutputType.HTML) {
            // TODO
        } else {
            Log.errorX(this, 'Invalid output file type.');
        }
    }

    private async takeActionByMode(mode: string, outputDir:string, type: string, nodeId?: number): Promise<void> {
        const analyzer = this.analyzer as ArkStatAnalyzer;
        let outputEntity: IOutput = OutputFactory.getOutput(type);
        let outputCfg = this.getOutputCfg(outputEntity, undefined, outputDir, '');
        switch (mode) {
            // get detached nodes
            case '1': 
                {
                    let nodes = await analyzer.getDetachedNodes();
                    if (nodes.length === 0) {
                        Log.errorX(this, `No detached nodes found.`);
                        break;
                    }
                    outputCfg.desc = { 
                        "Snapshot": analyzer.getSnapshotFilePath(), 
                        "Action:": "Get Detached Nodes",
                        "NodeLength": nodes.length,
                        "Time": Date.now()
                    };
                    outputCfg.output.filename = `detached-nodes_${Date.now()}`;
                    this.outputNodes(nodes, outputEntity, outputCfg);
                    break;
                }
            // get gc roots
            case '2':
                {
                    let nodes = await analyzer.getGCRoots();
                    if (nodes.length === 0) {
                        Log.errorX(this, "No gc roots found.");
                        break;
                    }
                    outputCfg.desc = { 
                        "FilePath": analyzer.getSnapshotFilePath(), 
                        "Action:": "Get GC Roots",
                        "NodeLength": nodes.length,
                        "Time": Date.now()
                    };
                    outputCfg.output.filename = `gc-roots_${Date.now()}`;
                    this.outputNodes(nodes, outputEntity, outputCfg);
                    break;
                }
            // get shortest path to node
            case '3':
                {
                    if (nodeId) {
                        let shortestPath = await analyzer.getShortestPathToNode(nodeId);
                        if  (!shortestPath) { 
                            Log.errorX(this, `Failed to find shortest path to node ${nodeId}`);
                            break;
                        }
                        let nodeTracePath = new ArkNodeTracePath(shortestPath, true);
                        outputCfg.desc = { 
                            "FilePath": analyzer.getSnapshotFilePath(), 
                            "Action:": "Get Shortest Path to Node",
                            "NodeID": nodeId,
                            "ShortestPath": nodeTracePath.uid,
                            "Time": Date.now()
                        };  
                        outputCfg.output.filename = `shortest-path-to-node${nodeId}_${Date.now()}`;
                        this.outputArkNodeTracePath(nodeTracePath, outputEntity, outputCfg);
                    } else {
                        Log.errorX(this, 'Node id is required.')
                    }           
                    break;
                }
            // get ark nodes/paths
            case '4':
                {
                    let arkNodesMap = await analyzer.getArkNodesMapByConfig();
                    if (!arkNodesMap) {
                        Log.errorX(this, `Failed to find ark nodes map by configuration`);
                        break;
                    }
                    outputCfg.desc = { 
                        "FilePath": analyzer.getSnapshotFilePath(), 
                        "Action:": "Get Aggration Paths",
                        "Time": Date.now()
                    };
                    outputCfg.output.filename = `aggration-paths_${Date.now()}`;
                    this.outputAggrationPath(arkNodesMap, outputEntity, outputCfg);
                    break;
                }
            // get node by configuration
            case '5':
                {
                    let nodes = await analyzer.getNodesByConfig();
                    if (nodes.length === 0) {
                        Log.errorX(this, `No nodes found by configuration.`);
                        break;
                    }
                    outputCfg.desc = { 
                        "FilePath": analyzer.getSnapshotFilePath(), 
                        "Action:": "Get Nodes by Configuration",
                        "NodeLength": nodes.length,
                        "Time": Date.now()
                    };
                    this.outputNodes(nodes, outputEntity, outputCfg);
                    break;
                }
            case '6':
                {
                    let map = await analyzer.getSharedHeapNodesMap();
                    if (map.size === 0) {
                        Log.errorX(this, `No shared heap node found.`);
                        break;
                    }
                    outputCfg.desc = { 
                        "FilePath": analyzer.getSnapshotFilePath(), 
                        "Action:": "Get Shared Heap Nodes Map",
                        "NodesMapLength": map.size,
                        "Time": Date.now()
                    };
                    outputCfg.output.filename = `shared-heap-nodes_${Date.now()}`;
                    map.forEach((nodes, key) => {
                        this.outputNodes(nodes, outputEntity, outputCfg);
                    })
                    break;
                }
            default:
                Log.warnX(this, `Unknown mode: ${mode}`);
                break;
        }
    }
}